/* eslint-disable no-undef */
/**
 * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
 * SPDX-FileCopyrightText: 2012-2016 ownCloud, Inc.
 * SPDX-License-Identifier: AGPL-3.0-or-later
 */

import axios, { isAxiosError } from '@nextcloud/axios'
import { showError } from '@nextcloud/dialogs'
import { t } from '@nextcloud/l10n'
import { addPasswordConfirmationInterceptors, PwdConfirmationMode } from '@nextcloud/password-confirmation'
import { generateUrl } from '@nextcloud/router'
import _ from 'underscore'

// we cannot use this as we need the global jQuery here for select2
// import $ from 'jquery'

addPasswordConfirmationInterceptors(axios)

/**
 * Returns the selection of applicable users in the given configuration row
 *
 * @param $row configuration row
 * @return array array of user names
 */
function getSelection($row) {
	let values = $row.find('.applicableUsers').select2('val')
	if (!values || values.length === 0) {
		values = []
	}
	return values
}

/**
 *
 * @param $row
 */
function getSelectedApplicable($row) {
	const users = []
	const groups = []
	const multiselect = getSelection($row)
	$.each(multiselect, function(index, value) {
		// FIXME: don't rely on string parts to detect groups...
		const pos = (value.indexOf) ? value.indexOf('(group)') : -1
		if (pos !== -1) {
			groups.push(value.substr(0, pos))
		} else {
			users.push(value)
		}
	})

	// FIXME: this should be done in the multiselect change event instead
	$row.find('.applicable')
		.data('applicable-groups', groups)
		.data('applicable-users', users)

	return { users, groups }
}

/**
 *
 * @param $element
 * @param highlight
 */
function highlightBorder($element, highlight) {
	$element.toggleClass('warning-input', highlight)
	return highlight
}

/**
 *
 * @param $input
 */
function isInputValid($input) {
	const optional = $input.hasClass('optional')
	switch ($input.attr('type')) {
		case 'text':
		case 'password':
			if ($input.val() === '' && !optional) {
				return false
			}
			break
	}
	return true
}

/**
 *
 * @param $input
 */
function highlightInput($input) {
	switch ($input.attr('type')) {
		case 'text':
		case 'password':
			return highlightBorder($input, !isInputValid($input))
	}
}

/**
 * Initialize select2 plugin on the given elements
 *
 * @param {Array<object>} array of jQuery elements
 * @param $elements
 * @param {number} userListLimit page size for result list
 */
function initApplicableUsersMultiselect($elements, userListLimit) {
	const escapeHTML = function(text) {
		return text.toString()
			.split('&').join('&amp;')
			.split('<').join('&lt;')
			.split('>').join('&gt;')
			.split('"').join('&quot;')
			.split('\'').join('&#039;')
	}
	if (!$elements.length) {
		return
	}
	return $elements.select2({
		placeholder: t('files_external', 'Type to select account or group.'),
		allowClear: true,
		multiple: true,
		toggleSelect: true,
		dropdownCssClass: 'files-external-select2',
		// minimumInputLength: 1,
		ajax: {
			url: OC.generateUrl('apps/files_external/ajax/applicable'),
			dataType: 'json',
			quietMillis: 100,
			data(term, page) { // page is the one-based page number tracked by Select2
				return {
					pattern: term, // search term
					limit: userListLimit, // page size
					offset: userListLimit * (page - 1), // page number starts with 0
				}
			},
			results(data) {
				const results = []
				let userCount = 0 // users is an object

				// add groups
				$.each(data.groups, function(gid, group) {
					results.push({ name: gid + '(group)', displayname: group, type: 'group' })
				})
				// add users
				$.each(data.users, function(id, user) {
					userCount++
					results.push({ name: id, displayname: user, type: 'user' })
				})

				const more = (userCount >= userListLimit) || (data.groups.length >= userListLimit)
				return { results, more }
			},
		},
		initSelection(element, callback) {
			const users = {}
			users.users = []
			const toSplit = element.val().split(',')
			for (let i = 0; i < toSplit.length; i++) {
				users.users.push(toSplit[i])
			}

			$.ajax(OC.generateUrl('displaynames'), {
				type: 'POST',
				contentType: 'application/json',
				data: JSON.stringify(users),
				dataType: 'json',
			}).done(function(data) {
				const results = []
				if (data.status === 'success') {
					$.each(data.users, function(user, displayname) {
						if (displayname !== false) {
							results.push({ name: user, displayname, type: 'user' })
						}
					})
					callback(results)
				} else {
					// FIXME add error handling
				}
			})
		},
		id(element) {
			return element.name
		},
		formatResult(element) {
			const $result = $('<span><div class="avatardiv"></div><span>' + escapeHTML(element.displayname) + '</span></span>')
			const $div = $result.find('.avatardiv')
				.attr('data-type', element.type)
				.attr('data-name', element.name)
				.attr('data-displayname', element.displayname)
			if (element.type === 'group') {
				const url = OC.imagePath('core', 'actions/group')
				$div.html('<img width="32" height="32" src="' + url + '">')
			}
			return $result.get(0).outerHTML
		},
		formatSelection(element) {
			if (element.type === 'group') {
				return '<span title="' + escapeHTML(element.name) + '" class="group">' + escapeHTML(element.displayname + ' ' + t('files_external', '(Group)')) + '</span>'
			} else {
				return '<span title="' + escapeHTML(element.name) + '" class="user">' + escapeHTML(element.displayname) + '</span>'
			}
		},
		escapeMarkup(m) { return m }, // we escape the markup in formatResult and formatSelection
	}).on('select2-loaded', function() {
		$.each($('.avatardiv'), function(i, div) {
			const $div = $(div)
			if ($div.data('type') === 'user') {
				$div.avatar($div.data('name'), 32)
			}
		})
	}).on('change', function(event) {
		highlightBorder($(event.target).closest('.applicableUsersContainer').find('.select2-choices'), !event.val.length)
	})
}

/**
 * @param id
 * @class OCA.Files_External.Settings.StorageConfig
 *
 * @classdesc External storage config
 */
function StorageConfig(id) {
	this.id = id
	this.backendOptions = {}
}
// Keep this in sync with \OCA\Files_External\MountConfig::STATUS_*
StorageConfig.Status = {
	IN_PROGRESS: -1,
	SUCCESS: 0,
	ERROR: 1,
	INDETERMINATE: 2,
}
StorageConfig.Visibility = {
	NONE: 0,
	PERSONAL: 1,
	ADMIN: 2,
	DEFAULT: 3,
}
/**
 * @memberof OCA.Files_External.Settings
 */
StorageConfig.prototype = {
	_url: null,

	/**
	 * Storage id
	 *
	 * @type int
	 */
	id: null,

	/**
	 * Mount point
	 *
	 * @type string
	 */
	mountPoint: '',

	/**
	 * Backend
	 *
	 * @type string
	 */
	backend: null,

	/**
	 * Authentication mechanism
	 *
	 * @type string
	 */
	authMechanism: null,

	/**
	 * Backend-specific configuration
	 *
	 * @type Object.<string,object>
	 */
	backendOptions: null,

	/**
	 * Mount-specific options
	 *
	 * @type Object.<string,object>
	 */
	mountOptions: null,

	/**
	 * Creates or saves the storage.
	 *
	 * @param {Function} [options.success] success callback, receives result as argument
	 * @param {Function} [options.error] error callback
	 * @param options
	 */
	save(options) {
		let url = OC.generateUrl(this._url)
		let method = 'POST'
		if (_.isNumber(this.id)) {
			method = 'PUT'
			url = OC.generateUrl(this._url + '/{id}', { id: this.id })
		}

		this._save(method, url, options)
	},

	/**
	 * Private implementation of the save function (called after potential password confirmation)
	 *
	 * @param {string} method
	 * @param {string} url
	 * @param {{success: Function, error: Function}} options
	 */
	async _save(method, url, options) {
		try {
			const response = await axios.request({
				confirmPassword: PwdConfirmationMode.Strict,
				method,
				url,
				data: this.getData(),
			})
			const result = response.data
			this.id = result.id
			options.success(result)
		} catch (error) {
			options.error(error)
		}
	},

	/**
	 * Returns the data from this object
	 *
	 * @return {Array} JSON array of the data
	 */
	getData() {
		const data = {
			mountPoint: this.mountPoint,
			backend: this.backend,
			authMechanism: this.authMechanism,
			backendOptions: this.backendOptions,
			testOnly: true,
		}
		if (this.id) {
			data.id = this.id
		}
		if (this.mountOptions) {
			data.mountOptions = this.mountOptions
		}
		return data
	},

	/**
	 * Recheck the storage
	 *
	 * @param {Function} [options.success] success callback, receives result as argument
	 * @param {Function} [options.error] error callback
	 * @param options
	 */
	recheck(options) {
		if (!_.isNumber(this.id)) {
			if (_.isFunction(options.error)) {
				options.error()
			}
			return
		}
		$.ajax({
			type: 'GET',
			url: OC.generateUrl(this._url + '/{id}', { id: this.id }),
			data: { testOnly: true },
			success: options.success,
			error: options.error,
		})
	},

	/**
	 * Deletes the storage
	 *
	 * @param {Function} [options.success] success callback
	 * @param {Function} [options.error] error callback
	 * @param options
	 */
	async destroy(options) {
		if (!_.isNumber(this.id)) {
			// the storage hasn't even been created => success
			if (_.isFunction(options.success)) {
				options.success()
			}
			return
		}

		try {
			await axios.request({
				method: 'DELETE',
				url: OC.generateUrl(this._url + '/{id}', { id: this.id }),
				confirmPassword: PwdConfirmationMode.Strict,
			})
			options.success()
		} catch (e) {
			options.error(e)
		}
	},

	/**
	 * Validate this model
	 *
	 * @return {boolean} false if errors exist, true otherwise
	 */
	validate() {
		if (this.mountPoint === '') {
			return false
		}
		if (!this.backend) {
			return false
		}
		if (this.errors) {
			return false
		}
		return true
	},
}

/**
 * @param id
 * @class OCA.Files_External.Settings.GlobalStorageConfig
 * @augments OCA.Files_External.Settings.StorageConfig
 *
 * @classdesc Global external storage config
 */
function GlobalStorageConfig(id) {
	this.id = id
	this.applicableUsers = []
	this.applicableGroups = []
}
/**
 * @namespace OCA.Files_External.Settings
 */
GlobalStorageConfig.prototype = _.extend(
	{},
	StorageConfig.prototype,
	/** @lends OCA.Files_External.Settings.GlobalStorageConfig.prototype */
	{
		_url: 'apps/files_external/globalstorages',

		/**
		 * Applicable users
		 *
		 * @type Array.<string>
		 */
		applicableUsers: null,

		/**
		 * Applicable groups
		 *
		 * @type Array.<string>
		 */
		applicableGroups: null,

		/**
		 * Storage priority
		 *
		 * @type int
		 */
		priority: null,

		/**
		 * Returns the data from this object
		 *
		 * @return {Array} JSON array of the data
		 */
		getData() {
			const data = StorageConfig.prototype.getData.apply(this, arguments)
			return _.extend(data, {
				applicableUsers: this.applicableUsers,
				applicableGroups: this.applicableGroups,
				priority: this.priority,
			})
		},
	},
)

/**
 * @param id
 * @class OCA.Files_External.Settings.UserStorageConfig
 * @augments OCA.Files_External.Settings.StorageConfig
 *
 * @classdesc User external storage config
 */
function UserStorageConfig(id) {
	this.id = id
}
UserStorageConfig.prototype = _.extend(
	{},
	StorageConfig.prototype,
	/** @lends OCA.Files_External.Settings.UserStorageConfig.prototype */
	{
		_url: 'apps/files_external/userstorages',
	},
)

/**
 * @param id
 * @class OCA.Files_External.Settings.UserGlobalStorageConfig
 * @augments OCA.Files_External.Settings.StorageConfig
 *
 * @classdesc User external storage config
 */
function UserGlobalStorageConfig(id) {
	this.id = id
}
UserGlobalStorageConfig.prototype = _.extend(
	{},
	StorageConfig.prototype,
	/** @lends OCA.Files_External.Settings.UserStorageConfig.prototype */
	{

		_url: 'apps/files_external/userglobalstorages',
	},
)

/**
 * @class OCA.Files_External.Settings.MountOptionsDropdown
 *
 * @classdesc Dropdown for mount options
 *
 * @param {object} $container container DOM object
 */
function MountOptionsDropdown() {
}
/**
 * @memberof OCA.Files_External.Settings
 */
MountOptionsDropdown.prototype = {
	/**
	 * Dropdown element
	 *
	 * @member Object
	 */
	$el: null,

	/**
	 * Show dropdown
	 *
	 * @param {object} $container container
	 * @param {object} mountOptions mount options
	 * @param {Array} visibleOptions enabled mount options
	 */
	show($container, mountOptions, visibleOptions) {
		if (MountOptionsDropdown._last) {
			MountOptionsDropdown._last.hide()
		}

		const $el = $(OCA.Files_External.Templates.mountOptionsDropDown({
			mountOptionsEncodingLabel: t('files_external', 'Compatibility with Mac NFD encoding (slow)'),
			mountOptionsEncryptLabel: t('files_external', 'Enable encryption'),
			mountOptionsPreviewsLabel: t('files_external', 'Enable previews'),
			mountOptionsSharingLabel: t('files_external', 'Enable sharing'),
			mountOptionsFilesystemCheckLabel: t('files_external', 'Check for changes'),
			mountOptionsFilesystemCheckOnce: t('files_external', 'Never'),
			mountOptionsFilesystemCheckDA: t('files_external', 'Once every direct access'),
			mountOptionsReadOnlyLabel: t('files_external', 'Read only'),
			deleteLabel: t('files_external', 'Disconnect'),
		}))
		this.$el = $el

		const storage = $container[0].parentNode.className

		this.setOptions(mountOptions, visibleOptions, storage)

		this.$el.appendTo($container)

		this._initialOptions = JSON.stringify(this.getOptions())
		MountOptionsDropdown._last = this

		this.$el.trigger('show')
	},

	hide() {
		if (this.$el) {
			this.$el.trigger('hide')
			this.$el.remove()
			this.$el = null
			MountOptionsDropdown._last = null
		}
	},

	/**
	 * Returns the mount options from the dropdown controls
	 *
	 * @return {object} options mount options
	 */
	getOptions() {
		const options = {}

		this.$el.find('input, select').each(function() {
			const $this = $(this)
			const key = $this.attr('name')
			let value = null
			if ($this.attr('type') === 'checkbox') {
				value = $this.prop('checked')
			} else {
				value = $this.val()
			}
			if ($this.attr('data-type') === 'int') {
				value = parseInt(value, 10)
			}
			options[key] = value
		})
		return options
	},

	/**
	 * Sets the mount options to the dropdown controls
	 *
	 * @param {object} options mount options
	 * @param {Array} visibleOptions enabled mount options
	 * @param storage
	 */
	setOptions(options, visibleOptions, storage) {
		if (storage === 'owncloud') {
			const ind = visibleOptions.indexOf('encrypt')
			if (ind > 0) {
				visibleOptions.splice(ind, 1)
			}
		}
		const $el = this.$el
		_.each(options, function(value, key) {
			const $optionEl = $el.find('input, select').filterAttr('name', key)
			if ($optionEl.attr('type') === 'checkbox') {
				if (_.isString(value)) {
					value = (value === 'true')
				}
				$optionEl.prop('checked', !!value)
			} else {
				$optionEl.val(value)
			}
		})
		$el.find('.optionRow').each(function(i, row) {
			const $row = $(row)
			const optionId = $row.find('input, select').attr('name')
			if (visibleOptions.indexOf(optionId) === -1 && !$row.hasClass('persistent')) {
				$row.hide()
			} else {
				$row.show()
			}
		})
	},
}

/**
 * @class OCA.Files_External.Settings.MountConfigListView
 *
 * @classdesc Mount configuration list view
 *
 * @param {object} $el DOM object containing the list
 * @param {object} [options]
 * @param {number} [options.userListLimit] page size in applicable users dropdown
 */
function MountConfigListView($el, options) {
	this.initialize($el, options)
}

MountConfigListView.ParameterFlags = {
	OPTIONAL: 1,
	USER_PROVIDED: 2,
	HIDDEN: 4,
}

MountConfigListView.ParameterTypes = {
	TEXT: 0,
	BOOLEAN: 1,
	PASSWORD: 2,
}

/**
 * @namespace OCA.Files_External.Settings
 */
MountConfigListView.prototype = _.extend({

	/**
	 * jQuery element containing the config list
	 *
	 * @type Object
	 */
	$el: null,

	/**
	 * Storage config class
	 *
	 * @type Class
	 */
	_storageConfigClass: null,

	/**
	 * Flag whether the list is about user storage configs (true)
	 * or global storage configs (false)
	 *
	 * @type bool
	 */
	_isPersonal: false,

	/**
	 * Page size in applicable users dropdown
	 *
	 * @type int
	 */
	_userListLimit: 30,

	/**
	 * List of supported backends
	 *
	 * @type Object.<string,Object>
	 */
	_allBackends: null,

	/**
	 * List of all supported authentication mechanisms
	 *
	 * @type Object.<string,Object>
	 */
	_allAuthMechanisms: null,

	_encryptionEnabled: false,

	/**
	 * @param {object} $el DOM object containing the list
	 * @param {object} [options]
	 * @param {number} [options.userListLimit] page size in applicable users dropdown
	 */
	initialize($el, options) {
		this.$el = $el
		this._isPersonal = ($el.data('admin') !== true)
		if (this._isPersonal) {
			this._storageConfigClass = OCA.Files_External.Settings.UserStorageConfig
		} else {
			this._storageConfigClass = OCA.Files_External.Settings.GlobalStorageConfig
		}

		if (options && !_.isUndefined(options.userListLimit)) {
			this._userListLimit = options.userListLimit
		}

		this._encryptionEnabled = options.encryptionEnabled
		this._canCreateLocal = options.canCreateLocal

		// read the backend config that was carefully crammed
		// into the data-configurations attribute of the select
		this._allBackends = this.$el.find('.selectBackend').data('configurations')
		this._allAuthMechanisms = this.$el.find('#addMountPoint .authentication').data('mechanisms')

		this._initEvents()
	},

	/**
	 * Custom JS event handlers
	 * Trigger callback for all existing configurations
	 *
	 * @param callback
	 */
	whenSelectBackend(callback) {
		this.$el.find('tbody tr:not(#addMountPoint):not(.externalStorageLoading)').each(function(i, tr) {
			const backend = $(tr).find('.backend').data('identifier')
			callback($(tr), backend)
		})
		this.on('selectBackend', callback)
	},
	whenSelectAuthMechanism(callback) {
		const self = this
		this.$el.find('tbody tr:not(#addMountPoint):not(.externalStorageLoading)').each(function(i, tr) {
			const authMechanism = $(tr).find('.selectAuthMechanism').val()
			callback($(tr), authMechanism, self._allAuthMechanisms[authMechanism].scheme)
		})
		this.on('selectAuthMechanism', callback)
	},

	/**
	 * Initialize DOM event handlers
	 */
	_initEvents() {
		const self = this

		const onChangeHandler = _.bind(this._onChange, this)
		// this.$el.on('input', 'td input', onChangeHandler);
		this.$el.on('keyup', 'td input', onChangeHandler)
		this.$el.on('paste', 'td input', onChangeHandler)
		this.$el.on('change', 'td input:checkbox', onChangeHandler)
		this.$el.on('change', '.applicable', onChangeHandler)

		this.$el.on('click', '.status>span', function() {
			self.recheckStorageConfig($(this).closest('tr'))
		})

		this.$el.on('click', 'td.mountOptionsToggle .icon-delete', function() {
			self.deleteStorageConfig($(this).closest('tr'))
		})

		this.$el.on('click', 'td.save>.icon-checkmark', function() {
			self.saveStorageConfig($(this).closest('tr'))
		})

		this.$el.on('click', 'td.mountOptionsToggle>.icon-more', function() {
			$(this).attr('aria-expanded', 'true')
			self._showMountOptionsDropdown($(this).closest('tr'))
		})

		this.$el.on('change', '.selectBackend', _.bind(this._onSelectBackend, this))
		this.$el.on('change', '.selectAuthMechanism', _.bind(this._onSelectAuthMechanism, this))

		this.$el.on('change', '.applicableToAllUsers', _.bind(this._onChangeApplicableToAllUsers, this))
	},

	_onChange(event) {
		const $target = $(event.target)
		if ($target.closest('.dropdown').length) {
			// ignore dropdown events
			return
		}
		highlightInput($target)
		const $tr = $target.closest('tr')
		this.updateStatus($tr, null)
	},

	_onSelectBackend(event) {
		const $target = $(event.target)
		let $tr = $target.closest('tr')

		const storageConfig = new this._storageConfigClass()
		storageConfig.mountPoint = $tr.find('.mountPoint input').val()
		storageConfig.backend = $target.val()
		$tr.find('.mountPoint input').val('')

		$tr.find('.selectBackend').prop('selectedIndex', 0)

		const onCompletion = $.Deferred()
		$tr = this.newStorage(storageConfig, onCompletion)
		$tr.find('.applicableToAllUsers').prop('checked', false).trigger('change')
		onCompletion.resolve()

		$tr.find('td.configuration').children().not('[type=hidden]').first().focus()
		this.saveStorageConfig($tr)
	},

	_onSelectAuthMechanism(event) {
		const $target = $(event.target)
		const $tr = $target.closest('tr')
		const authMechanism = $target.val()

		const onCompletion = $.Deferred()
		this.configureAuthMechanism($tr, authMechanism, onCompletion)
		onCompletion.resolve()

		this.saveStorageConfig($tr)
	},

	_onChangeApplicableToAllUsers(event) {
		const $target = $(event.target)
		const $tr = $target.closest('tr')
		const checked = $target.is(':checked')

		$tr.find('.applicableUsersContainer').toggleClass('hidden', checked)
		if (!checked) {
			$tr.find('.applicableUsers').select2('val', '', true)
		}

		this.saveStorageConfig($tr)
	},

	/**
	 * Configure the storage config with a new authentication mechanism
	 *
	 * @param {jQuery} $tr config row
	 * @param {string} authMechanism
	 * @param {$.Deferred} onCompletion
	 */
	configureAuthMechanism($tr, authMechanism, onCompletion) {
		const authMechanismConfiguration = this._allAuthMechanisms[authMechanism]
		const $td = $tr.find('td.configuration')
		$td.find('.auth-param').remove()

		$.each(authMechanismConfiguration.configuration, _.partial(this.writeParameterInput, $td, _, _, ['auth-param']).bind(this))

		this.trigger(
			'selectAuthMechanism',
			$tr,
			authMechanism,
			authMechanismConfiguration.scheme,
			onCompletion,
		)
	},

	/**
	 * Create a config row for a new storage
	 *
	 * @param {StorageConfig} storageConfig storage config to pull values from
	 * @param {$.Deferred} onCompletion
	 * @param {boolean} deferAppend
	 * @return {jQuery} created row
	 */
	newStorage(storageConfig, onCompletion, deferAppend) {
		let mountPoint = storageConfig.mountPoint
		let backend = this._allBackends[storageConfig.backend]

		if (!backend) {
			backend = {
				name: 'Unknown: ' + storageConfig.backend,
				invalid: true,
			}
		}

		// FIXME: Replace with a proper Handlebar template
		const $template = this.$el.find('tr#addMountPoint')
		const $tr = $template.clone()
		if (!deferAppend) {
			$tr.insertBefore($template)
		}

		$tr.data('storageConfig', storageConfig)
		$tr.show()
		$tr.find('td.mountOptionsToggle, td.save, td.remove').removeClass('hidden')
		$tr.find('td').last().removeAttr('style')
		$tr.removeAttr('id')
		$tr.find('select#selectBackend')
		if (!deferAppend) {
			initApplicableUsersMultiselect($tr.find('.applicableUsers'), this._userListLimit)
		}

		if (storageConfig.id) {
			$tr.data('id', storageConfig.id)
		}

		$tr.find('.backend').text(backend.name)
		if (mountPoint === '') {
			mountPoint = this._suggestMountPoint(backend.name)
		}
		$tr.find('.mountPoint input').val(mountPoint)
		$tr.addClass(backend.identifier)
		$tr.find('.backend').data('identifier', backend.identifier)

		if (backend.invalid || (backend.identifier === 'local' && !this._canCreateLocal)) {
			$tr.find('[name=mountPoint]').prop('disabled', true)
			$tr.find('.applicable,.mountOptionsToggle').empty()
			$tr.find('.save').empty()
			if (backend.invalid) {
				this.updateStatus($tr, false, t('files_external', 'Unknown backend: {backendName}', { backendName: backend.name }))
			}
			return $tr
		}

		const selectAuthMechanism = $('<select class="selectAuthMechanism"></select>')
		const neededVisibility = (this._isPersonal) ? StorageConfig.Visibility.PERSONAL : StorageConfig.Visibility.ADMIN
		$.each(this._allAuthMechanisms, function(authIdentifier, authMechanism) {
			if (backend.authSchemes[authMechanism.scheme] && (authMechanism.visibility & neededVisibility)) {
				selectAuthMechanism.append($('<option value="' + authMechanism.identifier + '" data-scheme="' + authMechanism.scheme + '">' + authMechanism.name + '</option>'))
			}
		})
		if (storageConfig.authMechanism) {
			selectAuthMechanism.val(storageConfig.authMechanism)
		} else {
			storageConfig.authMechanism = selectAuthMechanism.val()
		}
		$tr.find('td.authentication').append(selectAuthMechanism)

		const $td = $tr.find('td.configuration')
		$.each(backend.configuration, _.partial(this.writeParameterInput, $td).bind(this))

		this.trigger('selectBackend', $tr, backend.identifier, onCompletion)
		this.configureAuthMechanism($tr, storageConfig.authMechanism, onCompletion)

		if (storageConfig.backendOptions) {
			$td.find('input, select').each(function() {
				const input = $(this)
				const val = storageConfig.backendOptions[input.data('parameter')]
				if (val !== undefined) {
					if (input.is('input:checkbox')) {
						input.prop('checked', val)
					}
					input.val(storageConfig.backendOptions[input.data('parameter')])
					highlightInput(input)
				}
			})
		}

		let applicable = []
		if (storageConfig.applicableUsers) {
			applicable = applicable.concat(storageConfig.applicableUsers)
		}
		if (storageConfig.applicableGroups) {
			applicable = applicable.concat(_.map(storageConfig.applicableGroups, function(group) {
				return group + '(group)'
			}))
		}
		if (applicable.length) {
			$tr.find('.applicableUsers').val(applicable).trigger('change')
			$tr.find('.applicableUsersContainer').removeClass('hidden')
		} else {
			// applicable to all
			$tr.find('.applicableUsersContainer').addClass('hidden')
		}
		$tr.find('.applicableToAllUsers').prop('checked', !applicable.length)

		const priorityEl = $('<input type="hidden" class="priority" value="' + backend.priority + '" />')
		$tr.append(priorityEl)

		if (storageConfig.mountOptions) {
			$tr.find('input.mountOptions').val(JSON.stringify(storageConfig.mountOptions))
		} else {
			// FIXME default backend mount options
			$tr.find('input.mountOptions').val(JSON.stringify({
				encrypt: true,
				previews: true,
				enable_sharing: false,
				filesystem_check_changes: 1,
				encoding_compatibility: false,
				readonly: false,
			}))
		}

		return $tr
	},

	/**
	 * Load storages into config rows
	 */
	loadStorages() {
		const self = this

		const onLoaded1 = $.Deferred()
		const onLoaded2 = $.Deferred()

		this.$el.find('.externalStorageLoading').removeClass('hidden')
		$.when(onLoaded1, onLoaded2).always(() => {
			self.$el.find('.externalStorageLoading').addClass('hidden')
		})

		if (this._isPersonal) {
			// load userglobal storages
			$.ajax({
				type: 'GET',
				url: OC.generateUrl('apps/files_external/userglobalstorages'),
				data: { testOnly: true },
				contentType: 'application/json',
				success(result) {
					result = Object.values(result)
					const onCompletion = $.Deferred()
					let $rows = $()
					result.forEach(function(storageParams) {
						let storageConfig
						const isUserGlobal = storageParams.type === 'system' && self._isPersonal
						storageParams.mountPoint = storageParams.mountPoint.substr(1) // trim leading slash
						if (isUserGlobal) {
							storageConfig = new UserGlobalStorageConfig()
						} else {
							storageConfig = new self._storageConfigClass()
						}
						_.extend(storageConfig, storageParams)
						const $tr = self.newStorage(storageConfig, onCompletion, true)

						// userglobal storages must be at the top of the list
						$tr.detach()
						self.$el.prepend($tr)

						const $authentication = $tr.find('.authentication')
						$authentication.text($authentication.find('select option:selected').text())

						// disable any other inputs
						$tr.find('.mountOptionsToggle, .remove').empty()
						$tr.find('input:not(.user_provided), select:not(.user_provided)').attr('disabled', 'disabled')

						if (isUserGlobal) {
							$tr.find('.configuration').find(':not(.user_provided)').remove()
						} else {
							// userglobal storages do not expose configuration data
							$tr.find('.configuration').text(t('files_external', 'Admin defined'))
						}

						// don't recheck config automatically when there are a large number of storages
						if (result.length < 20) {
							self.recheckStorageConfig($tr)
						} else {
							self.updateStatus($tr, StorageConfig.Status.INDETERMINATE, t('files_external', 'Automatic status checking is disabled due to the large number of configured storages, click to check status'))
						}
						$rows = $rows.add($tr)
					})
					initApplicableUsersMultiselect(self.$el.find('.applicableUsers'), this._userListLimit)
					self.$el.find('tr#addMountPoint').before($rows)
					const mainForm = $('#files_external')
					if (result.length === 0 && mainForm.attr('data-can-create') === 'false') {
						mainForm.hide()
						$('a[href="#external-storage"]').parent().hide()
						$('.emptycontent').show()
					}
					onCompletion.resolve()
					onLoaded1.resolve()
				},
			})
		} else {
			onLoaded1.resolve()
		}

		const url = this._storageConfigClass.prototype._url

		$.ajax({
			type: 'GET',
			url: OC.generateUrl(url),
			contentType: 'application/json',
			success(result) {
				result = Object.values(result)
				const onCompletion = $.Deferred()
				let $rows = $()
				result.forEach(function(storageParams) {
					storageParams.mountPoint = (storageParams.mountPoint === '/') ? '/' : storageParams.mountPoint.substr(1) // trim leading slash
					const storageConfig = new self._storageConfigClass()
					_.extend(storageConfig, storageParams)
					const $tr = self.newStorage(storageConfig, onCompletion, true)

					// don't recheck config automatically when there are a large number of storages
					if (result.length < 20) {
						self.recheckStorageConfig($tr)
					} else {
						self.updateStatus($tr, StorageConfig.Status.INDETERMINATE, t('files_external', 'Automatic status checking is disabled due to the large number of configured storages, click to check status'))
					}
					$rows = $rows.add($tr)
				})
				initApplicableUsersMultiselect($rows.find('.applicableUsers'), this._userListLimit)
				self.$el.find('tr#addMountPoint').before($rows)
				onCompletion.resolve()
				onLoaded2.resolve()
			},
		})
	},

	/**
	 * @param {jQuery} $td
	 * @param {string} parameter
	 * @param {string} placeholder
	 * @param {Array} classes
	 * @return {jQuery} newly created input
	 */
	writeParameterInput($td, parameter, placeholder, classes) {
		const hasFlag = function(flag) {
			return (placeholder.flags & flag) === flag
		}
		classes = $.isArray(classes) ? classes : []
		classes.push('added')
		if (hasFlag(MountConfigListView.ParameterFlags.OPTIONAL)) {
			classes.push('optional')
		}

		if (hasFlag(MountConfigListView.ParameterFlags.USER_PROVIDED)) {
			if (this._isPersonal) {
				classes.push('user_provided')
			} else {
				return
			}
		}

		let newElement

		const trimmedPlaceholder = placeholder.value
		if (hasFlag(MountConfigListView.ParameterFlags.HIDDEN)) {
			newElement = $('<input type="hidden" class="' + classes.join(' ') + '" data-parameter="' + parameter + '" />')
		} else if (placeholder.type === MountConfigListView.ParameterTypes.PASSWORD) {
			newElement = $('<input type="password" class="' + classes.join(' ') + '" data-parameter="' + parameter + '" placeholder="' + trimmedPlaceholder + '" />')
		} else if (placeholder.type === MountConfigListView.ParameterTypes.BOOLEAN) {
			const checkboxId = _.uniqueId('checkbox_')
			newElement = $('<div><label><input type="checkbox" id="' + checkboxId + '" class="' + classes.join(' ') + '" data-parameter="' + parameter + '" />' + trimmedPlaceholder + '</label></div>')
		} else {
			newElement = $('<input type="text" class="' + classes.join(' ') + '" data-parameter="' + parameter + '" placeholder="' + trimmedPlaceholder + '" />')
		}

		if (placeholder.defaultValue) {
			if (placeholder.type === MountConfigListView.ParameterTypes.BOOLEAN) {
				newElement.find('input').prop('checked', placeholder.defaultValue)
			} else {
				newElement.val(placeholder.defaultValue)
			}
		}

		if (placeholder.tooltip) {
			newElement.attr('title', placeholder.tooltip)
		}

		highlightInput(newElement)
		$td.append(newElement)
		return newElement
	},

	/**
	 * Gets the storage model from the given row
	 *
	 * @param $tr row element
	 * @return {OCA.Files_External.StorageConfig} storage model instance
	 */
	getStorageConfig($tr) {
		let storageId = $tr.data('id')
		if (!storageId) {
			// new entry
			storageId = null
		}

		let storage = $tr.data('storageConfig')
		if (!storage) {
			storage = new this._storageConfigClass(storageId)
		}
		storage.errors = null
		storage.mountPoint = $tr.find('.mountPoint input').val()
		storage.backend = $tr.find('.backend').data('identifier')
		storage.authMechanism = $tr.find('.selectAuthMechanism').val()

		const classOptions = {}
		const configuration = $tr.find('.configuration input')
		const missingOptions = []
		$.each(configuration, function(index, input) {
			const $input = $(input)
			const parameter = $input.data('parameter')
			if ($input.attr('type') === 'button') {
				return
			}
			if (!isInputValid($input) && !$input.hasClass('optional')) {
				missingOptions.push(parameter)
				return
			}
			if ($(input).is(':checkbox')) {
				if ($(input).is(':checked')) {
					classOptions[parameter] = true
				} else {
					classOptions[parameter] = false
				}
			} else {
				classOptions[parameter] = $(input).val()
			}
		})

		storage.backendOptions = classOptions
		if (missingOptions.length) {
			storage.errors = {
				backendOptions: missingOptions,
			}
		}

		// gather selected users and groups
		if (!this._isPersonal) {
			const multiselect = getSelectedApplicable($tr)
			const users = multiselect.users || []
			const groups = multiselect.groups || []
			const isApplicableToAllUsers = $tr.find('.applicableToAllUsers').is(':checked')

			if (isApplicableToAllUsers) {
				storage.applicableUsers = []
				storage.applicableGroups = []
			} else {
				storage.applicableUsers = users
				storage.applicableGroups = groups

				if (!storage.applicableUsers.length && !storage.applicableGroups.length) {
					if (!storage.errors) {
						storage.errors = {}
					}
					storage.errors.requiredApplicable = true
				}
			}

			storage.priority = parseInt($tr.find('input.priority').val() || '100', 10)
		}

		const mountOptions = $tr.find('input.mountOptions').val()
		if (mountOptions) {
			storage.mountOptions = JSON.parse(mountOptions)
		}

		return storage
	},

	/**
	 * Deletes the storage from the given tr
	 *
	 * @param $tr storage row
	 * @param Function callback callback to call after save
	 */
	deleteStorageConfig($tr) {
		const self = this
		const configId = $tr.data('id')
		if (!_.isNumber(configId)) {
			// deleting unsaved storage
			$tr.remove()
			return
		}
		const storage = new this._storageConfigClass(configId)

		OC.dialogs.confirm(
			t('files_external', 'Are you sure you want to disconnect this external storage?')
			+ ' '
			+ t('files_external', 'It will make the storage unavailable in {instanceName} and will lead to a deletion of these files and folders on any sync client that is currently connected but will not delete any files and folders on the external storage itself.', {
				storage: this.mountPoint,
				instanceName: window.OC.theme.name,
			}),
			t('files_external', 'Delete storage?'),
			function(confirm) {
				if (confirm) {
					self.updateStatus($tr, StorageConfig.Status.IN_PROGRESS)

					storage.destroy({
						success() {
							$tr.remove()
						},
						error(result) {
							const statusMessage = (result && result.responseJSON) ? result.responseJSON.message : undefined
							self.updateStatus($tr, StorageConfig.Status.ERROR, statusMessage)
						},
					})
				}
			},
		)
	},

	/**
	 * Saves the storage from the given tr
	 *
	 * @param $tr storage row
	 * @param Function callback callback to call after save
	 * @param callback
	 * @param concurrentTimer only update if the timer matches this
	 */
	saveStorageConfig($tr, callback, concurrentTimer) {
		const self = this
		const storage = this.getStorageConfig($tr)
		if (!storage || !storage.validate()) {
			return false
		}

		this.updateStatus($tr, StorageConfig.Status.IN_PROGRESS)
		storage.save({
			success(result) {
				if (concurrentTimer === undefined
					|| $tr.data('save-timer') === concurrentTimer
				) {
					self.updateStatus($tr, result.status, result.statusMessage)
					$tr.data('id', result.id)

					if (_.isFunction(callback)) {
						callback(storage)
					}
				}
			},
			error(result) {
				if (concurrentTimer === undefined
					|| $tr.data('save-timer') === concurrentTimer
				) {
					const statusMessage = (result && result.responseJSON) ? result.responseJSON.message : undefined
					self.updateStatus($tr, StorageConfig.Status.ERROR, statusMessage)
				}
			},
		})
	},

	/**
	 * Recheck storage availability
	 *
	 * @param {jQuery} $tr storage row
	 * @return {boolean} success
	 */
	recheckStorageConfig($tr) {
		const self = this
		const storage = this.getStorageConfig($tr)
		if (!storage.validate()) {
			return false
		}

		this.updateStatus($tr, StorageConfig.Status.IN_PROGRESS)
		storage.recheck({
			success(result) {
				self.updateStatus($tr, result.status, result.statusMessage)
			},
			error(result) {
				const statusMessage = (result && result.responseJSON) ? result.responseJSON.message : undefined
				self.updateStatus($tr, StorageConfig.Status.ERROR, statusMessage)
			},
		})
	},

	/**
	 * Update status display
	 *
	 * @param {jQuery} $tr
	 * @param {number} status
	 * @param {string} message
	 */
	updateStatus($tr, status, message) {
		const $statusSpan = $tr.find('.status span')
		switch (status) {
			case null:
			// remove status
				$statusSpan.hide()
				break
			case StorageConfig.Status.IN_PROGRESS:
				$statusSpan.attr('class', 'icon-loading-small')
				break
			case StorageConfig.Status.SUCCESS:
				$statusSpan.attr('class', 'success icon-checkmark-white')
				break
			case StorageConfig.Status.INDETERMINATE:
				$statusSpan.attr('class', 'indeterminate icon-info-white')
				break
			default:
				$statusSpan.attr('class', 'error icon-error-white')
		}
		if (status !== null) {
			$statusSpan.show()
		}
		if (typeof message !== 'string') {
			message = t('files_external', 'Click to recheck the configuration')
		}
		$statusSpan.attr('title', message)
	},

	/**
	 * Suggest mount point name that doesn't conflict with the existing names in the list
	 *
	 * @param {string} defaultMountPoint default name
	 */
	_suggestMountPoint(defaultMountPoint) {
		const $el = this.$el
		const pos = defaultMountPoint.indexOf('/')
		if (pos !== -1) {
			defaultMountPoint = defaultMountPoint.substring(0, pos)
		}
		defaultMountPoint = defaultMountPoint.replace(/\s+/g, '')
		let i = 1
		let append = ''
		let match = true
		while (match && i < 20) {
			match = false
			$el.find('tbody td.mountPoint input').each(function(index, mountPoint) {
				if ($(mountPoint).val() === defaultMountPoint + append) {
					match = true
					return false
				}
			})
			if (match) {
				append = i
				i++
			} else {
				break
			}
		}
		return defaultMountPoint + append
	},

	/**
	 * Toggles the mount options dropdown
	 *
	 * @param {object} $tr configuration row
	 */
	_showMountOptionsDropdown($tr) {
		const self = this
		const storage = this.getStorageConfig($tr)
		const $toggle = $tr.find('.mountOptionsToggle')
		const dropDown = new MountOptionsDropdown()
		const visibleOptions = [
			'previews',
			'filesystem_check_changes',
			'enable_sharing',
			'encoding_compatibility',
			'readonly',
			'delete',
		]
		if (this._encryptionEnabled) {
			visibleOptions.push('encrypt')
		}
		dropDown.show($toggle, storage.mountOptions || [], visibleOptions)
		$('body').on('mouseup.mountOptionsDropdown', function(event) {
			const $target = $(event.target)
			if ($target.closest('.popovermenu').length) {
				return
			}
			dropDown.hide()
		})

		dropDown.$el.on('hide', function() {
			const newOptions = dropDown.getOptions()
			const newOptionsStr = JSON.stringify(newOptions)
			$('body').off('mouseup.mountOptionsDropdown')
			$tr.find('td.mountOptionsToggle>.icon-more').attr('aria-expanded', 'false')
			if (dropDown._initialOptions !== newOptionsStr) {
				$tr.find('input.mountOptions').val(newOptionsStr)
				self.saveStorageConfig($tr)
			}
		})
	},
}, OC.Backbone.Events)

window.addEventListener('DOMContentLoaded', function() {
	const enabled = $('#files_external').attr('data-encryption-enabled')
	const canCreateLocal = $('#files_external').attr('data-can-create-local')
	const encryptionEnabled = (enabled === 'true')
	const mountConfigListView = new MountConfigListView($('#externalStorage'), {
		encryptionEnabled,
		canCreateLocal: (canCreateLocal === 'true'),
	})
	mountConfigListView.loadStorages()

	// TODO: move this into its own View class
	const $allowUserMounting = $('#allowUserMounting')
	$allowUserMounting.bind('change', function() {
		OC.msg.startSaving('#userMountingMsg')
		if (this.checked) {
			OCP.AppConfig.setValue('files_external', 'allow_user_mounting', 'yes')
			$('input[name="allowUserMountingBackends\\[\\]"]').prop('checked', true)
			$('#userMountingBackends').removeClass('hidden')
			$('input[name="allowUserMountingBackends\\[\\]"]').eq(0).trigger('change')
		} else {
			OCP.AppConfig.setValue('files_external', 'allow_user_mounting', 'no')
			$('#userMountingBackends').addClass('hidden')
		}
		OC.msg.finishedSaving('#userMountingMsg', { status: 'success', data: { message: t('files_external', 'Saved') } })
	})

	$('input[name="allowUserMountingBackends\\[\\]"]').bind('change', function() {
		OC.msg.startSaving('#userMountingMsg')

		let userMountingBackends = $('input[name="allowUserMountingBackends\\[\\]"]:checked').map(function() {
			return $(this).val()
		}).get()
		const deprecatedBackends = $('input[name="allowUserMountingBackends\\[\\]"][data-deprecate-to]').map(function() {
			if ($.inArray($(this).data('deprecate-to'), userMountingBackends) !== -1) {
				return $(this).val()
			}
			return null
		}).get()
		userMountingBackends = userMountingBackends.concat(deprecatedBackends)

		OCP.AppConfig.setValue('files_external', 'user_mounting_backends', userMountingBackends.join())
		OC.msg.finishedSaving('#userMountingMsg', { status: 'success', data: { message: t('files_external', 'Saved') } })

		// disable allowUserMounting
		if (userMountingBackends.length === 0) {
			$allowUserMounting.prop('checked', false)
			$allowUserMounting.trigger('change')
		}
	})

	$('#global_credentials').on('submit', async function(event) {
		event.preventDefault()
		const $form = $(this)
		const $submit = $form.find('[type=submit]')
		$submit.val(t('files_external', 'Saving …'))

		const uid = $form.find('[name=uid]').val()
		const user = $form.find('[name=username]').val()
		const password = $form.find('[name=password]').val()

		try {
			await axios.request({
				method: 'POST',
				data: {
					uid,
					user,
					password,
				},
				url: generateUrl('apps/files_external/globalcredentials'),
				confirmPassword: PwdConfirmationMode.Strict,
			})

			$submit.val(t('files_external', 'Saved'))
			setTimeout(function() {
				$submit.val(t('files_external', 'Save'))
			}, 2500)
		} catch (error) {
			$submit.val(t('files_external', 'Save'))
			if (isAxiosError(error)) {
				const message = error.response?.data?.message || t('files_external', 'Failed to save global credentials')
				showError(t('files_external', 'Failed to save global credentials: {message}', { message }))
			}
		}

		return false
	})

	// global instance
	OCA.Files_External.Settings.mountConfig = mountConfigListView

	/**
	 * Legacy
	 *
	 * @namespace
	 * @deprecated use OCA.Files_External.Settings.mountConfig instead
	 */
	OC.MountConfig = {
		saveStorage: _.bind(mountConfigListView.saveStorageConfig, mountConfigListView),
	}
})

// export

OCA.Files_External = OCA.Files_External || {}
/**
 * @namespace
 */
OCA.Files_External.Settings = OCA.Files_External.Settings || {}

OCA.Files_External.Settings.GlobalStorageConfig = GlobalStorageConfig
OCA.Files_External.Settings.UserStorageConfig = UserStorageConfig
OCA.Files_External.Settings.MountConfigListView = MountConfigListView
